/**
 * NOTES:
 * 1, 一个 action 由一些连续 command|keyStroke|按键 组成
 * 2, 一个 action 中每个 command 可以处理一些小事情, 这些 command 来完成一个 action
 * 3, 一个 action 中每个 command 的对应处理函数可以向 this 中添加属性, 在处理完 action 后要 push 一个清理函数名, 来在 action 处理完后进行清理工作
 */
(function(){

function logger() {
    if (logger.LOG_LEVEL !== 'debug') {
        return;
    }

    var args = Array.prototype.slice.call(arguments);
    args.unshift(+new Date + ':');

    var log = window.console && console.log;
    if (log) {
        if (log.apply) {
            log.apply(console, args);
        } else {
            console.log(args);
        }
    } 
}
logger.LOG_LEVEL = 'release';

// from underscore
var utils = (function() {
    var arrayProto = Array.prototype;

    var nativeIndexOf = arrayProto.indexOf,
        nativeForEach = arrayProto.forEach,
        nativeMap     = arrayProto.map,
        nativeFilter  = arrayProto.filter,
        nativeEvery   = arrayProto.every,
        nativeTrim    = arrayProto.trim;

    var _ = {};

    _.indexOf = function(array, item) {
        if (array == null) {
            return -1;
        }

        if (nativeIndexOf && nativeIndexOf === array.indexOf) {
            return array.indexOf(item);
        } else {
            for (var i = 0, len = array.length; i < len; ++i) {
                if (item === array[i]) {
                    return i;
                }
            }

            return -1;
        }
    };

    _.in_array = function(item, array) {
        return _.indexOf(array, item) === -1 ? false : true;
    };

    _.forEach = function(obj, iterator, context) {
        if (obj == null) {
            return;
        }

        if (nativeForEach && nativeForEach === obj.forEach) {
            obj.forEach(iterator, context);
        } else if (obj instanceof Array) {
            for (var i = 0, len = obj.length; i < len; ++i) {
                if (iterator.call(context, obj[i], i, obj) === false) {
                    return;
                }
            }
        } else {
            for (var key in obj) {
                if (obj.hasOwnProperty(key)) {
                    if (iterator.call(context, obj[key], key, obj) === false) {
                        return;
                    }
                }
            }
        }
    };

    _.map = function(obj, iterator, context) {
        if (obj == null) {
            return;
        }

        if (nativeMap && nativeMap === obj.map) {
            return obj.map(iterator, context);
        } else {
            var results = obj instanceof Array ? [] : {};
            _.forEach(obj, function(value, key, list) {
                if (results instanceof Array) {
                    results.push(iterator.call(context, value, key, list));
                } else {
                    results[key] = iterator.call(context, value, key, list);
                }
            });

            return results;
        }
    };

    _.filter = function(obj, iterator, context) {
        if (obj == null) {
            return;
        }

        if (nativeFilter && nativeFilter === obj.filter) {
            return obj.filter(iterator, context);
        } else {
            var results = (obj instanceof Array) ? [] : {};

            utils.forEach(obj, function(value, index, list) {
                if (iterator.call(context, value, index, list)) {
                    if (results instanceof Array) {
                        results.push(value);
                    } else {
                        results[index] = value;
                    }
                }
            });

            return results;
        }
    };

    _.every = function(obj, iterator, context) {
        if (obj == null) {
            return true;
        }

        if (nativeEvery && nativeEvery == obj.every) {
            return obj.every(iterator, context);
        } else {
            var ret = true;
            utils.forEach(obj, function(value, index, list) {
                if (!(ret = ret && iterator.call(context, value, index, list))) {
                    return false;
                }
            });

            return ret;
        }
    };

    _.isEmptyObject = function(obj) {
        var isEmptyObject = true;

        for (var p in obj) {
            if (obj.hasOwnProperty(p)) {
                isEmptyObject = false;
                break;
            }
        }

        return isEmptyObject;
    };

    _.trim = function(str) {
        var TRIM_REG = /^\s+|\s+$/g;

        str = String(str);
        if (nativeTrim && nativeTrim === str.trim) {
            return str.trim(str);
        } else {
            return str.replace(TRIM_REG, '');
        }
    };

    _.upperFirst = function(str) {
        str = String(str);
        return str.charAt(0).toUpperCase() + str.substr(1);
    };

    return _;
})();

function Proto(__constructor, proto) {
    this.constructor = __constructor;
    if (proto instanceof Object) {
        for (var p in proto) {
            if (proto.hasOwnProperty(p)) {
                this[p] = proto[p];
            }
        }
    }
}

/** 
 * Event helper
 */
var Event = {
    addListener: function() {
        if (document.addEventListener) {
            return function(node, type, fn) {
                node.addEventListener(type, fn, false);
            }
        } else if (document.attachEvent) {
            return function(node, type, fn) {
                node.attachEvent('on' + type, fn);
            }
        } else {
            return function(node, type, fn) {
                throw 'cannot bind event"' + type + '"';
            }
        }
    }(),

    removeListener: function() {
        if (document.removeEventListener) {
            return function(node, type, fn) {
                node.removeEventListener(type, fn, false);
            }
        } else if (document.detachEvent) {
            return function(node, type, fn) {
                node.detachEvent('on' + type, fn);
            }
        } else {
            return function(node, type) {
                throw 'cannot remove event"' + type + '"';
            }
        }
    }(),

    /**
     * return Event object with normalized interface
     */
    EventObject: function(event) {
        event = event || window.event;
        
        // TODO: test why?
        // WHY: prototype error
        // var F = function() {};
        // F.prototype = event;
        // var e = new F();
        // e.originalEvent = event;

        // COPY property
        var e = {};
        e.originalEvent = event;
        var props = 'type altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which'.split(' ');
        var l = props.length, prop;
        while (l) {
            prop = props[--l];
            e[prop] = event[prop];
        }

        
        // normalize if necessary
        if (!e.target) {
            e.target = event.srcElement || document;
        }
        
        // add which for key events
        if (e.which === undefined) {
            e.which = (event.charCode !== undefined) ? event.charCode : event.keyCode;
        }

        if (!e.stopPropagation) {
            e.stopPropagation = function() {
                event.cancelBubble = true;
            }
        }
        if (!e.preventDefault) {
            e.preventDefault = function() {
                event.returnValue = false;
            }
        }

        return e;
    },

    // from https://developer.mozilla.org/en/DOM/document.createEvent
    fireClick: function(ele) {
        // hack for so safe Firefox
        if (/Firefox/.test(navigator.userAgent)) {
            logger('[Event.fireClick], firefox, special click');
            var attr_target = ele.getAttribute('target');
            if (!attr_target || attr_target == '_self') { // self tab
                location.href = ele.href;
            } else { // new tab
                window.open(ele.href);
            }
            return;
        }

        if (document.createEvent) {
            var evt = document.createEvent("MouseEvents");
            evt.initMouseEvent("click", true, true, window,
            0, 0, 0, 0, 0, false, false, false, false, 0, null);
            var canceled = ! ele.dispatchEvent(evt);
            if(canceled) {
                // A handler called preventDefault
                logger("[Event.fireClick], canceled");
            } else {
                // None of the handlers called preventDefault
                logger("[Event.fireClick], not canceled");
            }
        } else {
            ele.click();
        }
    }
};

var DOM = {
    /**
     * 元素是否被隐藏了 (display:none|visibility:hidden|祖先被隐藏)
     */
    isVisible: function(ele) {
        var rect = ele.getBoundingClientRect();
        var props = ['top', 'right', 'bottom', 'left'];

        var ret;
        ret = utils.every(props, function(value, index) {
            if (rect[value] === 0) {
                return true;
            }
        });

        return !ret;
    },

    isInView: function(ele) {
        if (!DOM.isVisible(ele)) { // 被隐藏
            return false;
        } else {
            var rect = ele.getBoundingClientRect();
            var props = ['top'];

            var ret;
            // 上面
            ret = utils.every(props, function(value, index) {
                if (rect[value] < 0) {
                    return true;
                }
            });
            if (ret) {
                return false;
            }

            // 下面
            var viewHeight = DOM.getViewHeight();
            ret = utils.every(props, function(value, index) {
                if (viewHeight - rect[value] <= 0) {
                    return true;
                }
            });
            if (ret) {
                return false;
            }

            return true;
        }
    },

    getElementsInView: function(tagName) {
        var eles;
        if (typeof tagName == 'string') {
            eles = document.getElementsByTagName(tagName);
        } else {
            eles = tagName;
        }

        var tmp = [];

        try {
            tmp = Array.prorotype.slice.call(eles);
        } catch(e) {
            var len = eles.length;
            // TODO: test reverse speed and unshift
            while (len--) {
                tmp.push(eles[len]);
            }
            tmp.reverse();
        }

        eles = utils.filter(tmp, function(ele, key) {
            if (DOM.isInView(ele)) {
                return true;
            }
        });

        return eles;
    },

    getElementPosition: function(ele) {
        var ele_rect = ele.getBoundingClientRect(ele);

        return {
            top : DOM.getDocScrollTop() + ele_rect.top,
            left: DOM.getDocScrollLeft() + ele_rect.left 
        };
    },

    getDocScrollTop: function() {
        var doc = document;
        return doc.documentElement.scrollTop || doc.body.scrollTop;
    },

    getDocScrollLeft: function() {
        var doc = document;
        return doc.documentElement.scrollLeft || doc.body.scrollLeft;
    },

    getViewHeight: function() {
        var doc = document,
            height = window.innerHeight;
            
        if (typeof height == 'undefined') {
            height = Math.max(doc.documentElement.clientHeight, doc.body.clientHeight);
        }

        return height;
    },

    getViewWidth: function() {
        var doc = document,
            width = window.innerWidth;
            
        if (typeof width == 'undefined') {
            width = Math.max(doc.documentElement.clientWidth, doc.body.clientWidth);
        }

        return width;
    },

    getDocHeight: function() {
        var doc = document;
        return doc.documentElement.scrollHeight || doc.body.scrollHeight;
    },

    addStyleSheet: function(clsText, attrs) {
        var doc   = document,
            style = doc.createElement('style');

        style.type = 'text/css';
        if (style.styleSheet) {
            style.styleSheet.cssText = clsText;
        } else {
            var rules = doc.createTextNode(clsText);
            style.appendChild(rules);
        }

        for (var p in attrs) {
            if (attrs.hasOwnProperty(p)) {
                style.setAttribute(p, attrs[p]);
            }
        }

        doc.body.appendChild(style);
    }
};

/** 
 * 一次按键
 */
function Command(evt) {
    this.event = evt;
}
Command.prototype = new Proto(Command, {
    isEscape: function() { // esc
        return this.getKeyCode() == 27;
    },

    isSpecialKey: function() { // 
        var keys = [33, 34, 35, 36, 37, 38, 39, 40]; // Home, End, PgUp, PgDn, four-arrow-key

        return utils.in_array(this.getKeyCode(), keys);
    },

    isValidKeyStroke: function() {
        var INVALID_TARGETS = ['input', 'textarea'];

        var tagName = this.event.target.tagName.toLowerCase();

        if (utils.in_array(tagName, INVALID_TARGETS)) {
            logger('[Command::isValidKeyStroke]', 'event target: "' + tagName + '", Invalid Keystroke!');
            return false;
        }

        logger('[Command::isValidKeyStroke]',  'event.target.tagName=' + tagName);
        return true;
    },

    getKeyCode: function() {
        return this.event.which;
    },

    getKeyStroke: function() {
        return String.fromCharCode(this.getKeyCode());
    },

    getEventType: function() {
        return this.event.type;
    },

    getEvent: function() {
        return this.event;
    }
});

function DocumentView() {
    this.currentKeyStrokes = ''; // command 按键序列
    this.actions; /* {} */       // {}; 符合按键的 action
    this.actionFinished = false; // action 是否结束
    this.destroyFuncs = [];      // [['fName', 'key', {}], []], action 结束的清理函数
    this.constructFuncs = [];    // [['fName', 'key', {}], []]
}
DocumentView.CONSTANTS = {
    SCROLL_STEP: 200,
    HELP_VIEW: {
        STYLE_ID: 'vimlike:bookmarklet:styleID',
        HTML_ID: 'vimlike:bookmarklet:htmlID',
        STYLE: '\
                .vim-bml-wrapper{width:100%;height:100%;overflow:hidden;border-radius:8px;background-color:#333;opacity:0.85;filter:alpha(opacity=85);}\
                .vim-bml-wrapper td,.vim-bml-wrapper th{background:transparent;color:#fff;font-family:arial,sans-serif;}\
                .vim-bml-area{margin:10px;}\
                .vim-bml-area td{vertical-align:top;}\
                .vim-bml-hd{width:100%;height:24px;border-bottom:1px solid #999;font-weight:bold;}\
                .vim-bml-lt{text-align:left;font-size:16px;}\
                .vim-bml-rt{text-align:right;cursor:pointer;text-decoration:underline;}\
                .vim-bml-btn{color:#dd0;}\
                .vim-bml-bd{border-collapse:collapse;width:100%;margin-top:3px;border-bottom:1px solid #999;font-size:13px;}\
                .vim-bml-help td{text-align:left;}\
                .vim-bml-key{text-align:right !important;font-weight:bold;padding-right:3px;}\
                .vim-bml-action{text-align:left;}\
                .vim-bml-help{border-collapse:collapse;}\
                .vim-bml-help th{color:#dd0;font-weight:bold;}\
                .vim-bml-help span{padding-right:3px;color:#dd0;font-family:"courier new",monospace;}\
                .vim-bml-ft{width:100%;margin-top:3px;font-size:12px;}\
                .vim-bml-ft-lt{text-align:left;}\
                .vim-bml-ft-lt a{color:#f60;}\
                .vim-bml-ft-rt{text-align:right;}',
        HTML: '\
                <div class="vim-bml-wrapper">\
                    <div class="vim-bml-area">\
                        <table class="vim-bml-hd">\
                            <tr>\
                                <td class="vim-bml-lt">Vim-like bookmarklet Help</td>\
                                <td class="vim-bml-rt" title="Click or Press Enter to hide Help"><span class="vim-bml-btn" id="vimlike:bookmarlet:closeBtn">close</span></td>\
                            </tr>\
                        </table>\
                        <table class="vim-bml-bd">\
                            <tr>\
                                <td>\
                                    <table class="vim-bml-help">\
                                        <tr><th></th><th>Supported</th></tr>\
                                        <tr><td class="vim-bml-key"><span>j</span>:</td><td>Scroll Down</td></tr>\
                                        <tr><td class="vim-bml-key"><span>k</span>:</td><td>Scroll Up</td></tr>\
                                        <tr><td class="vim-bml-key"><span>gg</span>:</td><td>Go to Top</td></tr>\
                                        <tr><td class="vim-bml-key"><span>G</span>:</td><td>Go to Bottom</td></tr>\
                                        <tr><td class="vim-bml-key"><span>g(\\d*)i</span>:</td><td>Focus Input</td></tr>\
                                        <tr><td class="vim-bml-key"><span>f</span>:</td><td>Click Link</td></tr>\
                                        <tr><td class="vim-bml-key"><span>F</span>:</td><td>Click Link in New Window</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Esc</span>:</td><td>Blur Input</td></tr>\
                                        <tr><td class="vim-bml-key"><span>?</span>:</td><td>Show This Help</td></tr>\
                                    </table>\
                                </td>\
                                <td>\
                                    <table class="vim-bml-help">\
                                        <tr><th></th><th>Native</th></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + f</span>:</td><td>Search</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + r</span>:</td><td>Refresh</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + w</span>:</td><td>Close the Current Window</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + l</span>:</td><td>Open URL in Current Window</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + h</span>:</td><td>View the History</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + -></span>:</td><td>History Forward</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + <-</span>:</td><td>History Back</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + Tab</span>:</td><td>Switches to the next tab.</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Ctrl + Shift + Tab</span>:</td><td>Switches to the previous tab</td></tr>\
                                        <tr><td class="vim-bml-key"><span>Alt + Home</span>:</td><td>Go to Home Page</td></tr>\
                                    </table>\
                                </td>\
                            </tr>\
                        </table>\
                        <table class="vim-bml-ft">\
                            <tr>\
                                <td class="vim-bml-ft-lt"><a href="mailto:myhere.2009@gmail.com">Feedback</a></td>\
                                <td class="vim-bml-ft-rt">Version: 0.1.0</td>\
                            </tr>\
                        </table>\
                    </div>\
                </div>',
        WIDTH: 800
    },
    FINDED_TAG_cssText: 'position:absolute;z-index:99999;background-color:yellow;color:black;padding:0 1px;border:solid 1px #E3BE23;text-decoration:none;font:bold 12px "Helvetica Neue", "Helvetica", "Arial", "Sans";',
    MSG_ELE_ID: 'vimlike:bookmarklet:welcomeID'
};
DocumentView.prototype = new Proto(DocumentView, {
    handle: function(cmd) {
        var type = cmd.getEventType();
        if (type == 'keypress') {
            logger('[DocumentView::handle]', 'event.type=keypress,', 'kyeCode:' + cmd.getKeyCode() ,'key: ' + cmd.getKeyStroke());
            this.filterActions(cmd);

            this.construct();
            this.router(cmd);
            this.destroy();
        } else if (type == 'keyup') {
            logger('[DocumentView::handle]', 'handle: event.type=keyup, key: ' + cmd.getKeyCode());
            if (cmd.isEscape() || cmd.isSpecialKey()) {
                if (cmd.isEscape()) {
                    this.blur();
                }

                this.actionFinished = true;
                this.construct();
                this.destroy();
            }
        }
    },

    /**
     * 过滤掉不符合目前按键的 action
     */
    filterActions: function(cmd) {
        var keyStroke    = cmd.getKeyStroke(),
            keyStrokes   = this.currentKeyStrokes + keyStroke;

        // 第一次击键使用原型上 actions 属性，之后原型上的被覆盖,
        // destroy() 中会 会删除 this 上的 actions 属性
        this.actions = utils.filter(this.actions, function(value, key) {
            var reg = new RegExp(key);
            if (reg.test(keyStrokes)) { // 符合按键顺序
                var fName = utils.trim(value),
                    cmdFnName = 'is' + utils.upperFirst(fName);

                if (!cmd.isValidKeyStroke()) { // 过滤掉 input, textarea 中 keypress 事件
                    return false;
                } else {
                    return true;
                }
            }
        });
        this.currentKeyStrokes = keyStrokes;
    },

    /**
     * 处理每个 command
     */
    router: function(cmd) {
        var keyStrokes     = this.currentKeyStrokes,
            actions = this.actions;

        logger('[DocumentView::router]', 'matched actions:', actions);
        if (utils.isEmptyObject(actions)) { // 不构成 action, 清除 command
            this.actionFinished = true;
            return;
        }

        var self = this;
        utils.forEach(actions, function(value, key, list) {
            var fName = utils.trim(value);

            logger('[DocumentView::router]', 'action commands: ' + keyStrokes + '..' +  fName);

            if (typeof self[fName] == 'function') {
                self[fName](cmd);
            }
        });
    },

    construct: function() {
        logger('[DocumentView::construct]');
        var constructFuncs = this.constructFuncs,
            constructFunc;
        if (constructFuncs.length > 0) {
            while (constructFunc = constructFuncs.shift()) {
                if (typeof this[constructFunc[0]] === 'function') {
                    this[constructFunc[0]](constructFunc[1], constructFunc[2])
                }
            }
        }
    },

    /**
     * 清除 command
     */
    destroy: function() {
        if (this.actionFinished) {
            logger('[DocumentView::destroy]');
            this.currentKeyStrokes = '';
            this.actionFinished    = false;
            delete this.actions;

            var destroyFuncs = this.destroyFuncs,
                destroyFunc,
                i = 0;

            while (destroyFunc = destroyFuncs[i++]) {
                if (typeof this[destroyFunc[0]] === 'function') {
                    this[destroyFunc[0]](destroyFunc[1], destroyFunc[2]);
                }
            }

            this.destroyFuncs = [];
        }
    },

    /**
     * action 开始的时候调用
     */
    setConstructFunc: function(fName, key, value) {
        this.constructFuncs.push([fName, key, value]);
    },

    /**
     * 设置清理函数, 用来清理 keyStroke 中设置的属性(恢复 this 的状态)
     */
    setDestoryFunc: function(fName, key, value) {
        this.destroyFuncs.push([fName, key, value]);
    },

    /**
     * @key {RegExp}
     * @value {String}
     */
    actions: {
        '^j'                   : 'scrollDown',
        '^k'                   : 'scrollUp',
        '^G'                   : 'goBottom',
        //'^d'                   : 'closeWindow',
        '^\\?$'                : 'help',
        '^gg?$'                : 'goTop',
        '^g(?:[1-9]\\d*)?i?$'  : 'goInsert',
        '^f.*'                 : 'find',
        '^F.*'                 : 'find',
        '^zh?a?n?g?l?i?n?$'    : 'sayHello'
    },

    // scrollDown, gotoBottom, scrollUp 对是否滚到尽头不做处理, 大于尽头的值 window.scrollTo() 没有影响
    scrollDown: function() { // j
        logger('[DocumentView::scrollDown]');

        var scrollTop = DOM.getDocScrollTop();
        window.scrollTo(0, scrollTop + DocumentView.CONSTANTS.SCROLL_STEP);
        this.actionFinished = true;
    },

    goBottom: function() { // G
        logger('[DocumentView::goBottom] offsetHeight=' + offsetHeight);

        var offsetHeight = DOM.getDocHeight();
        window.scrollTo(0, offsetHeight);
        this.actionFinished = true;
    },

    goTop: function(cmd) { // gg
        logger('[DocumentView::goTop]');
        var currentKeyStrokes = this.currentKeyStrokes;
        if (currentKeyStrokes !== 'gg') {
            return;
        }

        window.scrollTo(0, 0);
        this.actionFinished = true;
    },

    scrollUp: function() { // k
        logger('[DocumentView::srcollUp]');

        var scrollTop = DOM.getDocScrollTop();
        window.scrollTo(0, scrollTop - DocumentView.CONSTANTS.SCROLL_STEP);
        this.actionFinished = true;
    },

    closeWindow: function() {
        // TODO: 浏览器关闭窗口
        window.close();
        this.actionFinished = true;
    },

    blur: function() {
        var activeElement,
            elements;

        // @see:  http://stackoverflow.com/questions/967096/using-jquery-to-test-if-an-input-has-focus
        if (activeElement = document.activeElement) {
            logger('[DocumentView::blur] native document.activeElement.blur()!');
            try {
                activeElement.blur();
            } catch(e) {}
        } else {
            logger('[DocumentView::blur] not support document.activeElement!');
            elements = document.getElementsByTagName('input');
            for (var i = 0; activeElement = elements[i]; ++i) {
                try {
                    activeElement.blur();
                } catch(e) {}
            }
        }
    },

    help: function(cmd) {
        this.setConstructFunc('__cleanHelpContainer__');

        var doc = document,
            HELP_VIEW = DocumentView.CONSTANTS.HELP_VIEW,
            helpContainer = doc.getElementById(HELP_VIEW.HTML_ID);

        if (!helpContainer) { // 不存在
            // 添加 style
            DOM.addStyleSheet(HELP_VIEW.STYLE, {
                id: HELP_VIEW.STYLE_ID
            });

            helpContainer = doc.createElement('div');
            helpContainer.id = HELP_VIEW.HTML_ID;
            helpContainer.innerHTML = HELP_VIEW.HTML;

            document.body.appendChild(helpContainer);

            // 绑定 close 函数
            bindHelpCloseBtn();
        }

        // 调整位置
        var WIDTH  = HELP_VIEW.WIDTH,
            left, top;
        left = (DOM.getViewWidth() - WIDTH) / 2;
        top  = DOM.getDocScrollTop() + 200;
        helpContainer.style.cssText = 'display:block;position:absolute;top:'+top+'px;left:'+left+'px;z-index:99999;width:'+WIDTH+'px;';

        this.actionFinished = true;
    },

    __cleanHelpContainer__: function() {
        var helpContainer = document.getElementById(DocumentView.CONSTANTS.HELP_VIEW.HTML_ID);
        if (helpContainer) {
            helpContainer.style.display = 'none';
        }
    },

    goInsert: function(cmd) {
        if (cmd.getKeyStroke() !== 'i') {
            return;
        }
        logger('[DocumentView::goInsert]');

        // 获取第几个
        var focusIndex = String(this.currentKeyStrokes).match(/\d+/);
        focusIndex = focusIndex && focusIndex[0];
        focusIndex = parseInt((focusIndex || 1), 10);

        var inputEles = DOM.getElementsInView('input');
        var INVALID_INPUT_TYPE = ['hidden'];
        inputEles = utils.filter(inputEles, function(inputEle, key) {
            if (utils.in_array(inputEle.type, INVALID_INPUT_TYPE)) {
                return false;
            } else {
                return true;
            }
        });

        logger('[DocumentView::goInsert]', inputEles);
        var inputEle = inputEles[focusIndex-1];
        if (inputEle) {
            // prevent insert
            setTimeout(function() {
                inputEle.focus();
                inputEle = null;
                // inputEle.style.background = 'red';
            }, 1);
        }

        this.actionFinished = true;
    },

    find: function() {
        var links,
            currentKeyStrokes = this.currentKeyStrokes;

        if (currentKeyStrokes.toLowerCase() == 'f') { // 'f' 编号
            links = document.links;
            links = DOM.getElementsInView(links);

            this.tagContainer = document.createElement('div');
            links = tagEachLink(links, this.tagContainer);
            this.findedLinkTagPair = links;

            this.setDestoryFunc('__cleanFind__'); // 清理

            if (links.length == 0) {
                this.actionFinished = true;
            }

            return;
        } else { // 筛选
            this.findedLinkTagPair = filterLinks(this.findedLinkTagPair, currentKeyStrokes, this.tagContainer); // 过滤 & 更新 tag
            links = this.findedLinkTagPair;
        }

        var len = links.length;
        if (len > 1) {
            return;
        } else if (len === 1){
            var click = function(ele, newTab) {
                var attr_target = ele.getAttribute('target');
                if (newTab) {
                    ele.setAttribute('target', '_blank');
                }

                Event.fireClick(ele);

                if (newTab) {
                    setTimeout(function() {
                        ele.setAttribute('target', attr_target);
                        ele = null;
                    }, 10);
                }
            };

            click(links[0][1], currentKeyStrokes.charAt(0) === 'F');
        }
        this.actionFinished = true;
    },

    __cleanFind__: function() {
        var links = this.findedLinkTagPair,
            tagContainer = this.tagContainer;

        delete this.tagContainer;
        delete this.findedLinkTagPair;

        document.body.removeChild(tagContainer);
    },

    sayHello: function() {
        var keyStrokes = this.currentKeyStrokes;
        if (keyStrokes == 'zhanglin') {
            alert('hello, you just hit my name: "zhanglin"! sorry for this alert');
            this.actionFinished = true;
        }
    },

    showMsg: function(msg, autoHide) {
        var msg_ele = document.getElementById(DocumentView.CONSTANTS.MSG_ELE_ID);

        if (!msg_ele) {
            msg_ele = document.createElement('div');
            msg_ele.id = DocumentView.CONSTANTS.MSG_ELE_ID;

            var cssText = '\
                position:absolute;z-index:9999999;\
                overflow:hidden;border-radius:8px;background-color:#333;opacity:0.85;filter:alpha(opacity=85);\
                color:#dd0;font-weight:bold;font-family: "微软雅黑";font-size:16px;text-align:center;';

            msg_ele.style.cssText = cssText;

            document.body.appendChild(msg_ele);
        }

        msg_ele.innerHTML = msg;

        var WIDTH       = 400,
            LINE_HEIGHT = 40,
            left, top;

        left = (DOM.getViewWidth() - WIDTH) / 2;
        top  = DOM.getDocScrollTop() + 200;

        msg_ele.style.width = WIDTH + 'px';
        msg_ele.style.lineHeight = LINE_HEIGHT + 'px';
        msg_ele.style.top  = top + 'px';
        msg_ele.style.left = left + 'px';
        msg_ele.style.display =  'block';

        if (autoHide) {
            // 保存 timer 到函数实例上
            clearTimeout(arguments.callee.MsgHideTimer);
            arguments.callee.MsgHideTimer = setTimeout(function() {
                msg_ele.style.display = 'none';
                msg_ele = null;
            }, 1000);
        }
    },

    showWelcome: function() {
        this.showMsg('Welcome to Vimlike Bookmarklet!', 1);
    },

    showGoodBye: function() {
        this.showMsg('Goodbye', 1);

        this.actionFinished = true;
        this.destroy();
        this.construct();
    }
});
/* DocumentView::find 用到的两个辅助函数 */
function filterLinks(findedLinkTagPair, currentKeyStrokes, tagContainer) {
    var suffix = currentKeyStrokes.substr(1);

    return utils.filter(findedLinkTagPair, function(pair, idx) {
        if (pair[0].indexOf(suffix) === 0) {
            return true;
        } else {
            // remove tag and link
            tagContainer.removeChild(pair[2]);
            pair[0] = pair[1] = pair[2] = null;
        }
    });
}
function tagEachLink(links, tagContainer) {
    var findedLinkTagPair = [];

    var keys = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'],
        z26s = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'],
        key_len = keys.length,
        dig_cnt = Number(links.length).toString(key_len).length;

    utils.forEach(links, function(link, index) {
        var digits = index.toString(key_len),
            vim_key = '',
            i = 0,
            k, idx;
        while (k = digits.charAt(i++)) {
            idx = utils.indexOf(z26s, k);
            vim_key += keys[idx];
        }

        if (vim_key.length < dig_cnt) {
            vim_key = (new Array(dig_cnt - vim_key.length + 1)).join('a') + vim_key;
        }

        var ins = document.createElement('ins');
        var cssText = DocumentView.CONSTANTS.FINDED_TAG_cssText;
        var ele_pos = DOM.getElementPosition(link);
        cssText += 'left:' + ele_pos.left + 'px;top:' + ele_pos.top + 'px;';
        ins.style.cssText = cssText;
        ins.innerHTML = vim_key;
        tagContainer.appendChild(ins);

        findedLinkTagPair.push([vim_key, link, ins]);
    });

    document.body.appendChild(tagContainer);

    return findedLinkTagPair;
}
/* DocumentView::help 绑定的关闭函数 */
function bindHelpCloseBtn() {
    var closeBtn = document.getElementById('vimlike:bookmarlet:closeBtn');

    if (closeBtn) {
        Event.addListener(closeBtn, 'click', function() {
            var helpContainer = document.getElementById(DocumentView.CONSTANTS.HELP_VIEW.HTML_ID);
            if (helpContainer) {
                helpContainer.style.display = 'none';
            }
        })
    }
}

function Controller(view) {
    this.view = view;

    this.bindedVim;
    this.handlers = {};
    this.setHandlers();
    this.bindEvents();
}
Controller.prototype = new Proto(Controller, {
    setHandlers: function() {
        var self = this;

        this.handlers.keypress = function(event) {
            var cmd = new Command(Event.EventObject(event));

            self.view.handle(cmd);
        };
        this.handlers.keyup = function(event) {
            var cmd = new Command(Event.EventObject(event));

            self.view.handle(cmd);
        };
    },

    bindEvents: function() {
        this.bindedVim = true;
        this.bindKeypress();
        this.bindKeyup();

        this.view.showWelcome();

    },

    unbindEvents: function() {
        this.bindedVim = false;
        this.unbindKeypress();
        this.unbindKeyup();

        this.view.showGoodBye();
    },

    toggle: function() {
        if (this.bindedVim) {
            this.unbindEvents();

            return false;
        } else {
            this.bindEvents();

            return true;
        }
    },

    bindKeypress: function() {
        Event.addListener(document, 'keypress', this.handlers.keypress);
    },

    unbindKeypress: function() {
        Event.removeListener(document, 'keypress', this.handlers.keypress);
    },

    bindKeyup: function() {
        Event.addListener(document, 'keyup', this.handlers.keyup);
    },

    unbindKeyup: function() {
        Event.removeListener(document, 'keyup', this.handlers.keyup);
    }
});

function extractToWindow(c) {
    window.VimBookmarklet = {
        toggleDebug: function() {
            if (logger.LOG_LEVEL === 'debug') {
                logger.LOG_LEVEL = 'release';
                return false;
            } else {
                logger.LOG_LEVEL = 'debug';
                return true;
            }
        },

        isDebug: function() {
            return logger.LOG_LEVEL === 'debug' ? true : false;
        },

        toggle: function() {
            return c.toggle();
        },

        isVimlikeOn: function() {
            return c.bindedVim;
        },

        showHelp: function() {
            if (c.bindedVim) {
                c.view.help();
            }
        }
    }
}

function init() {
    var view, 
        controller;

    view       = new DocumentView();
    controller = new Controller(view);

    extractToWindow(controller);
}

// must put at last
init();
// logger.LOG_LEVEL = 'debug';
logger('[Vim-like Bookmarklet]script loaded!');
})();
